
"""\
It's the original version for JPEG file structure analysis with the document
<a href="http://www.w3.org/Graphics/JPEG/itu-t81.pdf">ITU-T81</a>.
<pre>
  Symbol   Code Assignment    Description
----------------------------------------------------------------------
* Start of Frame makers, non-hierarchical Huffman coding
    SOF0         FFC0         Baseline DCT
    SOF1         FFC1         Extended sequential DCT
    SOF2         FFC2         Progressive DCT
    SOF3         FFC3         Spatial (sequential) lossless
* Start of Frame markers, hierarchical Huffman coding
    SOF5         FFC5         Differential sequential DCT
    SOF6         FFC6         Differential progressive DCT
    SOF7         FFC7         Differential spatial lossless
* Start of Frame markers, non-hierarchical arcithmetic coding
    JPG          FFC8         Reserved for JPEG extensions
    SOF9         FFC9         Extended sequential DCT
    SOF10        FFCA         Progressive DCT
    SOF11        FFCB         Spatial (sequential) lossless
* Start of Frame markers, hierarchical arthmetic coding
    SOF13        FFCD         Differential sequential DCT
    SOF14        FFCE         Differential progressive DCT
    SOF15        FFCF         Differential spatial lossless
* Huffman table specification
    DHT          FFC4         Define Huffman table(s)
* arithmetic coding conditioning specification
    DAC          FFCC         Define arithmetic conditioning table
* Restart interval termination
    RSTm         FFD0-FFD7    Restart with modulo 8 counter
* Other marker
    SOI          FFD8         Start of image
    EOI          FFD9         End of image
    SOS          FFDA         Start of scan
    DQT          FFDB         Define quantization table(s)
    DNL          FFDC         Define number of lines
    DRI          FFDD         Define restart interval
    DHP          FFDE         Define hierarchical progression
    EXP          FFDF         Expand reference image(s)
    APPn         FFE0-FFEF    Reserved for application
    JPGn         FFF0-FFFD    Reserved for JPEG extension
    COM          FFFFE        Comment
* Reserved markers
    TEM          FF01         For temporary use in arithmetic coding
    RES          FF02-FFBF    Reserved
</pre>

It suspports the EXIF parsring if the EXIF plug-in is installed and enabled \
and Adobe <a href="http://www.adobe.com/devnet/xmp/">XMP
</a> representation in XML format.
"""

import sys

__version__ = '0.23'

__aof__ = ('JPEG File Interchange Format', 'JPG')
__extension__ = (('JPeG Files', ('*.jpg', '*.jpe', '*.jpeg')),)


def __newItemObject(target, subject, content, size, kws):
    objectp = target.newItemObject(
        subject, content,
        field=target.newFieldObject(kws['start'] + kws['offset'], size))
    kws['offset'] += size

    return objectp


def __jpegMarkerStartOrEndOfImage(entry, source, target, notify, kws):
    _markerSO = {
        0xffd8: 'Start of Image', 0xffd9: 'End of Image'
    }

    marker = target.readValue(source, 2, **kws)
    entry.append(
        __newItemObject(target, 'Marker', '%s (0x%02x)' % (
            _markerSO[marker], marker), 2, kws))

    return True


def __jpegMarkerStartOfFrame(entry, source, target, notify, kws):

    # +------+----+---+---+---+----+-------------------------+
    # | SOFn | Lf | P | Y | X | Nf | Component-Specification |
    # +------+----+---+---+---+----+-------------------------+
    #
    # Component-Specification = C1 H1 V1 Tq1 ....
    #
    _markerSOF = {
        0xffc0: ('Start of Frame makers: non-hierarchical Huffman coding '
                 '- Baseline DCT (0xffc0)'),
        0xffc1: ('Start of Frame makers: non-hierarchical Huffman coding '
                 '- Extended sequential DCT (0xffc1)'),
        0xffc2: ('Start of Frame makers: non-hierarchical Huffman coding '
                 '- Progressive DCT (0xffc2)'),
        0xffc3: ('Start of Frame makers: non-hierarchical Huffman coding '
                 '- Spatial (sequential) lossless (0xffc3)'),
        0xffc9: ('Start of Frame markers: non-hierarchical arcithmetic coding '
                 '- Extended sequential DCT (0xffc9)'),
        0xffca: ('Start of Frame markers: non-hierarchical arcithmetic coding '
                 '- Progressive DCT (0xffca)'),
        0xffcb: ('Start of Frame markers: non-hierarchical arcithmetic coding '
                 '- Spatial (sequential) lossless (0xffcb)'),
        0xffde: 'Other marker: Define hierarchical progression (0xffde)'
    }

    def __markerSofName(nb, marker):
        return _markerSOF.get(marker)

    _header = (
        ('maker', 2, 'ignore', {'func': __markerSofName}),
        ('Length', 2, 'decimal,export'),
        ('Sample Precision (bits)', 1, 'decimal'),
        ('Lines', 2, 'decimal'),
        ('Samples per line', 2, 'decimal'),
        ('Number of image components in frame', 1, 'decimal'),
        ('HVTq', '$0 - 8', 'eval,recursive', {
            'subdescriptor': (
                ('q', 3, 'eval,recursive', {
                    'repeat': '($0-8)/3',
                    'subdescriptor': (
                        ('C', 1, 'value'),
                        ('H/V', 1, 'value'),
                        ('Tq', 1, 'value')
                    )}),
            )})
    )

    marker = target.readValue(source, 2, **kws)
    if marker in _markerSOF:
        ret, objectp, consumed, _, _ = target.parse(
            _markerSOF[marker], _header, afile=source, **kws)
        if ret:
            entry.append(objectp)
            kws['offset'] += consumed

    return ret


def __jpegMarkerStartOfScan(entry, source, target, notify, kws):

    # +-----+----+----+------------------------+----+----+----+----+
    # | SOS | Ls | Ns |Component-Specification | Ss | Se | Ah | Al |
    # +-----+----+----+------------------------+----+----+----+----+
    #
    # Component-Specification = Cs1 Td1 Ta1 ...
    _header = (
        ('marker', 2, 'ignore'),
        ('Ls', 2, 'value'),
        ('Ns', 1, 'decimal,export'),
        ('CTT', 2, 'eval,recursive', {
            'repeat': '$0',
            'subdescriptor': (
                ('Cs', 1, 'value'),
                ('Td/Ta', 1, 'value'),
            )}),
        ('Ss', 1, 'value'),
        ('Se', 1, 'value'),
        ('Ah/Al', 1, 'value')
    )

    ret, objectp, consumed, _, _ = target.parse(
        'Start of Segment header: Start of scan (0xffda)',
        _header, afile=source, **kws)

    if ret:
        entry.append(objectp)
        kws['offset'] += consumed

    return ret


def __jpegMarkerDefineQuantizationTable(entry, source, target, notify, kws):

    # Refer to Table b.6
    # +-----+----+-------+----+----+-----+-----+
    # | DQT | Lq | Pq/Tq | Q0 | Q1 | ... | Q63 |
    # +-----+----+-------+----+----+-----+-----+
    _header = (
        ('Marker', 2, 'ignore'),
        ('Length', 2, 'decimal'),
        ('Pq/Tq', 1, 'value'),
        ('DQT', 64, 'recursive', {
            'subdescriptor': (
                ('Q', 1, 'value', {'repeat': 64}),
            )})
    )

    ret, objectp, consumed, _, _ = target.parse(
        'Marker segment: Define quantization tables (0xffdb)',
        _header, afile=source, **kws)

    if ret:
        entry.append(objectp)
        kws['offset'] += consumed

    return ret


def __jpegMarkerHuffmanTableSpecification(entry, source, target, notify, kws):

    # Refer to Table b.7
    # +-----+----+-------+----+----+-----+-----+--------------------------+
    # | DHT | Lh | Tc/Th | L1 | L2 | ... | L16 | Symbol-length Assignment |
    # +-----+----+-------+----+----+-----+-----+--------------------------+
    #
    # Symbol-length Assignment = V1,1 V1,2 V1,L1 ... V16,L16
    _header = (
        ('marker', 2, 'ignore'),
        ('Length', 2, 'value'),
        ('Tc/h', 1, 'value'),
        ('Li', 16, 'string,export')
    )

    ret, objectp, consumed, _, exported = target.parse(
        'Marker segment: Define Huffman table(s) (0xffc4)',
        _header, afile=source, **kws)

    if ret:
        kws['offset'] += consumed
        offset = kws['offset']
        obj = target.newItemObject('DHT')
        for k in range(16):
            size = ord(exported[0][k])
            if size > 0:
                obj.appendChildren(__newItemObject(target, '#%d' % k,
                                   '', size, kws))

        obj.update(field=target.newFieldObject(kws['start'] + offset,
                   kws['offset'] - offset))
        # add the size of DHT list
        objectp.adjust(size=kws['offset'] - offset)
        objectp.append(obj)
        entry.append(objectp)

    return ret


def __jpegMarkerArithmeticConditioningTableSpecification(entry, source,
                                                         target, notify, kws):

    # Refer to Table b.8
    # +-----+----+-------+----+
    # | DAC | La | Tc/Tb | Cs |
    # +-----+----+-------+----+
    _header = (
        ('DAC', 2, 'value'),
        ('La', 2, 'value,export'),
        ('T/Cs', 2, 'recursive', {
            'repeat': '$0 / 2',
            'subdescriptor': (
                ('Tc/Tb', 2, 'value'),
                ('Cs', 2, 'value')
            )})
    )

    ret, objectp, consumed, _, _ = target.parse(
        'Marker segment: Define arithmetic conditioning table (0xffcc)',
        _header, afile=source, **kws)

    if ret:
        entry.append(objectp)
        kws['offset'] += consumed

    return ret


def __jpegMarkerRestartIntervalDefinition(entry, source, target, notify, kws):

    # Refer to Table b.8
    # +-----+----+----+
    # | DRI | Lr | Ri |
    # +-----+----+----+
    _header = (
        ('DRI', 2, 'value'),
        ('Lr', 2, 'value'),
        ('Ri', 2, 'value')
    )

    ret, objectp, consumed, _, _ = target.parse(
        'Marker segment: Define restart interval (0xffdd)',
        _header, afile=source, **kws)

    if ret:
        entry.append(objectp)
        kws['offset'] += consumed

    return ret


def __jpegMarkerComment(entry, source, target, notify, kws):

    # Refer to Table b.10
    # +-----+----+------------------+
    # | COM | Lc | Cm1 ... Cm(Lc-2) |
    # +-----+----+------------------+
    _header = (
        ('COM', 2, 'value'),
        ('Lc', 2, 'value,export'),
        ('Comment', '$0-2', 'string,eval')
    )

    ret, objectp, consumed, _, _ = target.parse('Comment (0xfffe)', _header,
                                                afile=source, **kws)
    if ret:
        entry.append(objectp)
        kws['offset'] += consumed

    return ret


def __jpegMarkerApplicationData(entry, source, target, notify, kws):

    def _jpegMarkAppJfifTriger(entry, data, target, notify, kws):
        _App0Jfif = (
            ('Identifier', 4, 'string,verify', {'verified': 'JFIF'}),
            ('_null_', 1, 'ignore'),
            ('Major', 1, 'value'),
            ('Minor', 1, 'value'),
            ('Unit', 1, 'value'),
            ('Xdensity', 2, 'decimal'),
            ('Ydensity', 2, 'decimal'),
            ('Xthumbnail', 1, 'decimal'),
            ('Ythumbnail', 1, 'decimal')
        )

        ret, objectp, consumed, _, _ = target.parse(
            'JFIF', _App0Jfif, data=data, _endian=False)

        return ret, objectp

    def _jpegMarkAppAdobeXmpTriger(entry, data, target, notify, kws):
        _XMP = 'http://ns.adobe.com/xap/1.0/'

        _App1Xap = (
            ('XMP', len(_XMP), 'string,verify', {'verified': _XMP}),
            ('_null_', 1, 'ignore'),
            ('XMP packet', len(data) - len(_XMP) - 1, 'object',
             {'attr': 'extractable,no_update'})
        )

        ret, objectp, consumed, _, _ = target.parse(
            'XAP', _App1Xap, data=data, _endian=False)

        return ret, objectp

    # http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart3.pdf
    def _jpegMarkAppAdobePhotoshopTriger(entry, data, target, notify, kws):
        _PS3 = 'Photoshop 3.0'

        k = len(_PS3) + 1
        datsz = len(data) - k

        _App13PS = (
            ('Signature', len(_PS3), 'string,verify', {'verified': _PS3}),
            ('_null_', 1, 'ignore'),
            ('Format of Photoshop image resources', datsz,
             'object,objexp')
        )

        ret, objectp, consumed, _, exp = target.parse(
            'Photoshop', _App13PS, data=data, _endian=False)

        def __readPascalString(data):
            ret, offset, zeros = '', 0, 0

            for offset, c in enumerate(data):
                if ord(c) != 0:
                    ret += c
                    zeros = 0
                else:
                    zeros += 1
                    if zeros == 2:
                        break

            if offset > 0:
                offset += 1

            return ret, offset

        while ret and k < datsz:
            _RESOURCE = (
                ('Type', 4, 'string'),
                ('Id', 2, 'value, export')
            )

            offset = k
            ret, obj, consumed, _, exported = target.parse(
                '', _RESOURCE, data=data[k:], _endian=False)

            if ret:
                obj.adjust(start=k)
                obj.update(subject='0x%02x' % exported[0])
                k += consumed

                name, consumed = __readPascalString(data[k:])
                obj.append(
                    target.newItemObject(
                        'Name', name,
                        field=target.newFieldObject(k, consumed)))
                k += consumed

                length = kws['_i'].atoi(data[k:k + 4], False)
                obj.append(
                    target.newItemObject(
                        'Length', length,
                        field=target.newFieldObject(k, 4)))
                k += 4

                obj.append(
                    target.newItemObject(
                        'Data', field=target.newFieldObject(k, length)))
                k += length
                if (k & 0x01) > 0:
                    k += 1

                obj.adjust(size=k - offset)
                exp[0].appendChildren(obj)

        return ret, objectp

    def _jpegMarkAppExifTriger(entry, data, target, notify, kws):
        # do implement the decoding via an Exif provider:-)
        invoker = kws.get('_i')
        if invoker is not None:
            objectp = invoker.referral(
                'EXF.decode', entry, target, data=data, _tgt=target)

            if objectp is not None:
                return True, objectp

        return False, None

    def _jpegMarkAppAdobeTriger(entry, data, target, notify, kws):

        def _adobeMakerTransform(nb, va):
            if va == 0:
                ret = 'CMYK'
            else:
                ret = 'YCCK'

            return ret

        # refer jpegsrc-7
        _App14Adobe = (
            ('Flag', 5, 'ignore'),
            ('Version', 2, 'value'),
            ('flags0', 2, 'value'),
            ('flags1', 2, 'value'),
            ('transform', 1, 'string', {'func': _adobeMakerTransform})
        )

        ret, objectp, consumed, _, _ = target.parse(
            'Adobe', _App14Adobe, data=data)

        return ret, objectp

    def jpegMarkAppXpxrTriger(entry, data, target, notify, kws):
        _App2Fpxr = (
            ('Flag', 5, 'ignore'),
            ('data', len(data) - 5, 'string')
        )

        ret, objectp, consumed, _, _ = target.parse(
            'Exif FlashPix APP2 application marker',
            _App2Fpxr, data=data)

        return ret, objectp

    def _appTrigerDispatcher(entry, data, kws):
        _AppTrigers = {
            'JFIF': _jpegMarkAppJfifTriger,
            'Exif': _jpegMarkAppExifTriger,
            'Adob': _jpegMarkAppAdobeTriger,
            'FPXR': jpegMarkAppXpxrTriger,
            'Photoshop 3.0': _jpegMarkAppAdobePhotoshopTriger,
            'http://ns.adobe.com/xap/1.0/': _jpegMarkAppAdobeXmpTriger
        }

        ret = False
        for app in _AppTrigers:
            if data[:len(app)] == app:
                # print 'App: %s' % app
                ret, objp = _AppTrigers[app](
                    entry, data, target, notify, kws)

                if ret:
                    objp.adjust(start=kws['start'] + kws['offset'] + 4)
                    entry.update(subject=app, content='')
                    entry.appendChildren(objp.getChildren())

                break

        return ret

    # Refer to Table b.10
    # +------+----+------------------+
    # | APPn | Lp | Ap1 ... Ap(Lp-2) |
    # +------+----+------------------+

    _AppData = (
        ('App', 2, 'value,export'),
        ('Length', 2, 'decimal,export'),
        ('App', '$1 - 2', 'eval,string,export,objexp')
    )

    ret, objectp, consumed, _, exported = target.parse(
        'APP', _AppData, afile=source, **kws)

    if ret:
        # handle the app structure
        _appTrigerDispatcher(exported[3], exported[2], kws)
        objectp.update(
            subject='Marker segment',
            content=' Reserved for application segments - %d (0x%x)' % (
                exported[0] - 0xffe0, exported[0]))

        entry.append(objectp)
        kws['offset'] += consumed

    return ret


def __jpegMarkerDefineNumberOfLines(entry, source, target, notify, kws):

    # Refer to Table b.8
    # +-----+----+----+
    # | DNL | Ld | NL |
    # +-----+----+----+

    _header = (
        ('DNL', 2, 'value'),
        ('Ld', 2, 'value'),
        ('NL', 2, 'value')
    )

    ret, objectp, consumed, _, _ = target.parse(
        'Marker segment: Define number of lines',
        _header, afile=source, **kws)

    if ret:
        entry.append(objectp)
        kws['offset'] += consumed

    return ret


def __jpegMarkerExpandReferenceImage(entry, source, target, notify, kws):

    # Refer to Table b.10
    # +-----+----+-------+
    # | EXP | Le | Eh/Ev |
    # +-----+----+-------+

    _header = (
        ('EXP', 2, 'value'),
        ('Le', 2, 'value,export'),
        ('Eh/v', 1, 'value')
    )

    ret, objectp, consumed, _, exported = target.parse(
        'Marker segment: Expand reference image(s) (0xffdf)',
        _header, afile=source, **kws)

    if ret:
        entry.append(objectp)
        kws['offset'] += exported + 2

    return ret


def _jpegMakerDispatcher(marker, entry, source, target, notify, kws):
    _markerTable = {
        (0xffd8, 0xffd9): __jpegMarkerStartOrEndOfImage,
        (0xffc0, 0xffc1, 0xffc2, 0xffc3, 0xffc9, 0xffca, 0xffcb, 0xffde):
            __jpegMarkerStartOfFrame,
        (0xffda,): __jpegMarkerStartOfScan,
        (0xffdb,): __jpegMarkerDefineQuantizationTable,
        (0xffc4,): __jpegMarkerHuffmanTableSpecification,
        (0xffcc,): __jpegMarkerArithmeticConditioningTableSpecification,
        (0xffdd,): __jpegMarkerRestartIntervalDefinition,
        (0xfffe,): __jpegMarkerComment,
        xrange(0xffe0, 0xfff0): __jpegMarkerApplicationData,
        (0xffdc,): __jpegMarkerDefineNumberOfLines,
        (0xffdf,): __jpegMarkerExpandReferenceImage
    }

    ret = False
    for m in _markerTable:
        if marker in m:
            # print 'MAKER: %x' % marker, "==>", _markerTable[m]
            ret = _markerTable[m](entry, source, target, notify, kws)

    return ret


class _JpegDecorder:
    def Execute(self, source, target, notify, **kws):
        ret = True

        target.setEndian('big-endian', kws)
        marker = target.readValue(source, 2, **kws)

        if marker != 0xffd8 or len(source) == 2:
            ret = False
        else:
            kws['offset'] = 0
            size = len(source)
            entry = target.getEntry()

            pblock = None
            lastObj = None
            while kws['offset'] < size:
                marker = target.readValue(source, 2, **kws)
                ret = _jpegMakerDispatcher(marker, entry,
                                           source, target, notify, kws)
                if ret:
                    pblock = None
                else:
                    # search an avaiable marker
                    flag = False
                    kws['offset'] += 1

                    MARK = chr(0xff)
                    while (not flag) and kws['offset'] < size:
                        offset = kws['offset'] - 1

                        ma = source.read(100000,
                                         kws.get('start', 0) + kws['offset'])
                        for k, c in enumerate(ma):
                            if c == MARK:
                                flag = True
                                kws['offset'] += k
                                break
                        else:
                            kws['offset'] += len(ma)

                        if pblock and pblock[0] + pblock[1] \
                                == kws['start'] + offset:
                            pblock[1] += kws['offset'] - offset
                            lastObj.adjust(size=pblock[1])
                        else:
                            pblock = [kws['start'] + offset,
                                      kws['offset'] - offset]
                            lastObj = target.newItemObject(
                                'Entropy-coded segment', '',
                                field=target.newFieldObject(
                                    pblock[0], pblock[1]))

                            entry.appendChildren(lastObj)

        return ret


def fmtService():
    return _JpegDecorder, 'picture'
